package som;

import java.util.Collections;
import java.util.List;

import utils.ExecData;

/**
 * Implements the algorithm which trains the SOM
 */
public class Algorithm {

	// constants for the SOM training
	private static final double START_LEARNING_RATE = 0.07;
	private static final int NUM_ITERATIONS = 7500;
	private static int NUM_CIRCLES = 2;
	private static final double[] NEIGHBORHOOD_RATES = {1,0.2,0.1,0.05}; // i'th element is update rate for cells on i'th circle
	public final static int BOARD_SIZE = 6;	
	
	private List<Sample> samples; // train examples
	private Board board; // the SOM cells
	private ExecData execData; // holds quantization and topological errors for each iteration
	private static final String ERROR_LOG = "error_log"; // name of CSV file which logs quantization and topological errors for each iteration 
	
	/**
	 * Constructor- initializes each cell of the SOM with a random sample
	 * @param samples train examples
	 */
	public Algorithm(List<Sample> samples)
	{
		this.samples = samples;
		board = new Board(BOARD_SIZE, BOARD_SIZE);		
		execData = new ExecData();
	}
	
	/**
	 * Trains the SOM cells for <code>NUM_ITERATIONS</code> epochs.
	 * Then assigns each train example to its best matching cell of the SOM
	 * @return final state of the SOM cells with their assigned train examples
	 */
	public Board run()
	{
		Cell cell; // best matching cell for current train examples
		Sample currSam; // current train example
		double lr = START_LEARNING_RATE; // initialize learning rate
		
		for(int i = 0 ; i < NUM_ITERATIONS ; ++i) // for specified number of epochs 
		{
		
			// set number of circles according to current iteration
			NUM_CIRCLES = calcNumCircles(i);
			
			// shuffle input samples
			Collections.shuffle(samples);
			
			// log current error
			calcCurrError();
			
			for(int index = 0 ; index < samples.size() ; ++index) // iterate over all train examples
			{
				// find best matching cell for current train example
				currSam = samples.get(index);
				cell = board.getBestMatchingCell(currSam);
				
				// update the cell
				updateCells(cell,currSam,lr);
				
				// if done training, assign the example to this cell
				if(i == (NUM_ITERATIONS-1) )
				{					
					cell.assignSample(currSam);
				}
			}
			lr = Math.exp(-(double) i / NUM_ITERATIONS); // let learning rate decay exponentially
		}
		
		// calculate final errors
		double quantError = board.quantError();
		double topologicalError = board.topologicalError();
		
		// display final errors
		System.out.println("Final quantization error: " + quantError);
		System.out.println("Final topological error: " + topologicalError);

		// log final error
		execData.addIterData(board.quantError(), board.topologicalError());		

		// create a CSV file logging the errors in each iteration
		execData.createsCsvFiles(ERROR_LOG);
		
		// return SOM's final state
		return board;
	}
	
	/**
	 * Calculates quantization and topological errors after assigning the train examples
	 * according to current state of the board.
	 * The error values are logged and then the assignments are cleared
	 */
	private void calcCurrError() {
	
		Cell cell; // best matching cell for current train examples
		Sample currSam; // current train example
		
		// assign each example to its best matching cell
		for(int index = 0 ; index < samples.size() ; ++index) // iterate over all train examples
		{
			// find best matching cell for current train example
			currSam = samples.get(index);
			cell = board.getBestMatchingCell(currSam);
			
			// assign example
			cell.assignSample(currSam);
		}
		
		// calculate quantization and topological errors
		double quantError = board.quantError();
		double topologicalError = board.topologicalError();
		
		// update error values for current iteration
		execData.addIterData(quantError, topologicalError);
		
		// clear assignment for each cell
		board.clearCells();
	}

	/**
	 * Updates the representing samples of the assigned cell and its neighbors
	 * @param cell assigned sample
	 * @param sampleToAssign sample to be assigned 
	 * @param lr learning rate
	 */
	public void updateCells(Cell cell , Sample sampleToAssign, double lr)
	{
		// for each cell on the circles to be updated
		for(int dist = 0 ; dist < NUM_CIRCLES ; ++dist)
		{
			// get assigned cell's neighbors on current circle
			List<Cell> neighborhood = board.getNeighborhoods(dist, cell);
			for(Cell c : neighborhood)
			{
				// update current cell
				c.updateRepresentingExam(sampleToAssign, lr, NEIGHBORHOOD_RATES[dist]);
			}
		}		
	}
	
	/**
	 * Returns number of circles to update, according to current iteration.
	 * Enables number of circles to decay as number of iteration grows
	 * @param iteration number of current iteration (epoch)
	 * @return number of circles to be updated
	 */
	private int calcNumCircles(int iteration) {
		
		if (iteration <= 200) {
			return 3;
		}
		
		if (iteration <= 3500) {
			return 2;
		}
		
		return 1;
	}
	
	/**
	 * Returns current state of the SOM
	 * @return the SOM
	 */
	public Board getBoard()
	{
		return board;
	}
}
